import * as d3 from "d3";
import { FlexHierarchyPointNode, flextree } from "d3-flextree";
import { each, find, has, map } from "lodash-es";
import { useEffect, useMemo, useRef, useState } from "react";
import PlanNode from "./planNode";
import { NodeProp } from "./utils/enums";
import { Node } from "./utils/interfaces";
import { PlanService } from "./utils/plan-service";
import useInView from "../../hooks/useView";

const initStyleConfig = {
  width: 211,
  height: 98,
  scale: 1,
  padding: 40,
  minScale: 0.2,
  linePadding: 60,
};

function drawMarker(svg: any) {
  svg
    .append("marker")
    .attr("id", "markerOfUp")
    .attr("markerUnits", "userSpaceOnUse")
    .attr("viewBox", "0 -5 10 10")
    .attr("refX", 10)
    .attr("refY", 0)
    .attr("markerWidth", 8)
    .attr("markerHeight", 8)
    .attr("orient", "auto")
    .attr("stroke-width", 1)
    .append("path")
    .attr("d", "M0,-5L10,0L0,5")
    .attr("fill", "#7d8da7");
}
function drawLine(link: any) {
  const source = link.source;
  const target = link.target;

  const linePadding = initStyleConfig.linePadding;
  const k = Math.abs(target.y - (source.y + source.ySize) - linePadding);
  const path = d3.path();
  path.moveTo(target.x, target.y);

  // control point
  const controlPoint1X = target.x;
  const controlPoint1Y = target.y - k / 4;
  const controlPoint2X = source.x;
  const controlPoint2Y = source.y + source.ySize - linePadding + 20;

  // three bezier curve
  path.bezierCurveTo(
    controlPoint1X,
    controlPoint1Y,
    controlPoint2X,
    controlPoint2Y,
    source.x,
    source.y + source.ySize - linePadding + 10
  );
  path.lineTo(source.x, source.y + source.ySize - linePadding);
  return path.toString();
}

function getLayoutExtent(
  layoutRootNode: FlexHierarchyPointNode<Node>
): [number, number, number, number] {
  const minX =
    d3.min(
      map(layoutRootNode.descendants(), (childNode) => {
        return childNode.x - childNode.xSize / 2;
      })
    ) || 0;

  const maxX =
    d3.max(
      map(layoutRootNode.descendants(), (childNode) => {
        return childNode.x + childNode.xSize / 2;
      })
    ) || 0;

  const minY =
    d3.min(
      map(layoutRootNode.descendants(), (childNode) => {
        return childNode.y;
      })
    ) || 0;

  const maxY =
    d3.max(
      map(layoutRootNode.descendants(), (childNode) => {
        return childNode.y + childNode.ySize;
      })
    ) || 0;
  return [minX, maxX, minY, maxY];
}

const planService = new PlanService();

export default function RPlan({
  planSource,
}: {
  planSource: string | Record<string, any>;
  fullscreenClick?: () => void;
  fullscreenButtonSrc?: string;
}) {
  let planJson = planSource as Record<string, any>;
  if (typeof planSource === "string") {
    planJson = JSON.parse(planSource);
  }
  const [transform, setTransform] = useState<any>("");

  const ctes = useRef<any[]>([]);
  const toCteLinks = useRef<any[]>([]);
  const svgRef = useRef<SVGSVGElement>(null);
  const [renderData, setRenderData] = useState<{
    rects: JSX.Element[];
    links: JSX.Element[];
    mainWithCteLink: JSX.Element[];
    cteAreas: JSX.Element[];
  }>({
    rects: [],
    links: [],
    mainWithCteLink: [],
    cteAreas: [],
  });
  let layout = flextree({
    children: (d: any) => {
      return d.Plans ? d.Plans : null;
    },
    nodeSize: (e: any) => {
      const { width, height, linePadding } = initStyleConfig;
      return [
        e.data.width || width,
        (e.data.height && e.data.height + linePadding) || height,
      ];
    },
    spacing: (nodeA, nodeB) => {
      // Left and right spacing of nodes
      return 30 * nodeA.path(nodeB).length;
    },
  });

  const zoomListener = d3
    .zoom()
    .scaleExtent([initStyleConfig.minScale, 3])
    .on("zoom", function (e:any) {
      initStyleConfig.scale = e.transform.k;
      setTransform(e.transform);
    });

  const rootNode = planJson.Plan;
  const plan = useMemo(() => {
    return planService.createPlan("", planJson as any, "");
  }, [planSource]);
  const rootTreeRef = useRef(layout.hierarchy(rootNode, (v) => v.Plans));

  let layoutRootNode: FlexHierarchyPointNode<any> = rootTreeRef.current;
  ctes.current = [];
  each(plan?.ctes, (cte: any) => {
    const tree = layout.hierarchy(cte, (v: Node) => v.Plans);
    ctes.current.push(tree);
  });

  function handleZoom() {
    d3.select(svgRef.current)
      .on("dblclick.zoom", null)
      .call(zoomListener as any);
    if (layoutRootNode) {
      const extent = getLayoutExtent(layoutRootNode);
      const x0 = extent[0];
      const y0 = extent[2];
      const x1 = extent[1];
      const y1 = extent[3];
      const rect = svgRef.current!.getBoundingClientRect();
      if (!rect.width) {
        return;
      }
      d3.select(svgRef.current)
        .transition()
        .call(
          zoomListener.transform as any,
          d3.zoomIdentity
            .translate(rect.width / 2, 10)
            .scale(
              initStyleConfig.scale !== 1
                ? initStyleConfig.scale
                : Math.min(
                    1,
                    Math.max(
                      initStyleConfig.minScale,
                      0.8 /
                        Math.max(
                          (x1 - x0) / rect.width,
                          (y1 - y0) / rect.height
                        )
                    )
                  )
            )
            .translate(-(x0 + x1) / 2, 10)
        );
    }
  }

  function handleLayout() {
    layoutRootNode = layout(rootTreeRef.current);
    const mainLayoutExtent = getLayoutExtent(layoutRootNode);

    const offset: [number, number] = [
      mainLayoutExtent[0],
      mainLayoutExtent[3] + initStyleConfig.padding,
    ];
    each(ctes.current, (tree) => {
      const cteRootNode = layout(tree);
      const currentCteExtent = getLayoutExtent(cteRootNode);
      const currentWidth = currentCteExtent[1] - currentCteExtent[0];
      cteRootNode.each((node) => {
        node.x += offset[0] - currentCteExtent[0];
        node.y += offset[1];
      });
      offset[0] += currentWidth + initStyleConfig.padding * 2;
    });

    // compute links from node to CTE
    toCteLinks.current = [];
    each(layoutRootNode.descendants(), (source) => {
      if (has(source.data, NodeProp.CTE_NAME)) {
        const cte = find(ctes.current, (cteNode) => {
          return (
            cteNode.data[NodeProp.SUBPLAN_NAME] ===
            "CTE " + source.data[NodeProp.CTE_NAME]
          );
        });
        if (cte) {
          toCteLinks.current.push({
            source: source,
            target: cte,
          });
        }
      }
    });

    // compute links from node in CTE to other CTE
    each(ctes.current, (cte) => {
      each(cte.descendants(), (sourceCte) => {
        if (has(sourceCte.data, NodeProp.CTE_NAME)) {
          const targetCte = find(ctes.current, (cteNode) => {
            return (
              cteNode.data[NodeProp.SUBPLAN_NAME] ===
              "CTE " + sourceCte.data[NodeProp.CTE_NAME]
            );
          });
          if (targetCte) {
            toCteLinks.current.push({
              source: sourceCte,
              target: targetCte,
            });
          }
        }
      });
    });
  }

  function updateRender() {
    const rects = layoutRootNode.descendants().map((e, i) => {
      return (
        <foreignObject
          key={i}
          width={e.xSize}
          height={1}
          x={e.x - e.xSize / 2}
          y={e.y}
          style={{ overflow: "visible" }}
        >
          <PlanNode data={e.data} updateNodeSize={updateNodeSize}></PlanNode>
        </foreignObject>
      );
    });
    const mainWithCteLink = toCteLinks.current.map((link, index) => {
      return (
        <path
          key={index}
          d={drawLine(link)}
          stroke="#7d8da7"
          strokeWidth={1}
          fill="none"
        ></path>
      );
    });
    //   drawLine
    const links = layoutRootNode.links().map((e, i) => {
      return (
        <path
          key={i}
          strokeWidth={1}
          d={drawLine(e)}
          stroke="#7d8da7"
          fill="none"
          markerEnd="url(#markerOfUp)"
        />
      );
    });
    const cteAreas = ctes.current.map((cte, i) => {
      return (
        <g key={i}>
          <rect
            x={getLayoutExtent(cte)[0] - initStyleConfig.padding / 4}
            y={getLayoutExtent(cte)[2] - initStyleConfig.padding / 2}
            width={
              getLayoutExtent(cte)[1] -
              getLayoutExtent(cte)[0] +
              initStyleConfig.padding / 2
            }
            height={getLayoutExtent(cte)[3] - getLayoutExtent(cte)[2]}
            stroke="#cfcfcf"
            strokeWidth={2}
            fill="#cfcfcf"
            fillOpacity="10%"
            rx="5"
            ry="5"
          ></rect>
          {cte.links().map((link: any, i: number) => {
            return (
              <path
                key={i}
                d={drawLine(link)}
                stroke="#7d8da7"
                strokeWidth={1}
                strokeLinecap="square"
                fill="none"
              />
            );
          })}
          {cte.descendants().map((item: any, index: number) => {
            return (
              <foreignObject
                key={index}
                x={item.x - item.xSize / 2}
                y={item.y}
                width={item.xSize}
                height="1"
                style={{ overflow: "visible" }}
              >
                <PlanNode
                  data={item.data}
                  updateNodeSize={updateNodeSize}
                ></PlanNode>
              </foreignObject>
            );
          })}
        </g>
      );
    });
    setRenderData({
      rects,
      links,
      cteAreas,
      mainWithCteLink,
    });
  }

  function updateNodeSize(node: Node, size: [number, number]) {
    node.width = size[0] / initStyleConfig.scale;
    node.height = size[1] / initStyleConfig.scale;
    handleLayout();
    updateRender();
  }

  function updateTreeData() {
    // update tree data
    const newTree = layout.hierarchy(rootNode, (v) => v.Plans);
    const descendantsNewTree = newTree.descendants();
    rootTreeRef.current
      .descendants()
      ?.forEach((node: { data: Record<string, any> }, i: number) => {
        const strOld = JSON.stringify(node.data);
        const strNew = JSON.stringify(descendantsNewTree[i].data);
        if (strOld !== strNew) {
          const { width, height } = node.data;
          node.data = { width, height, ...descendantsNewTree[i].data };
        }
      });
  }

  useEffect(() => {
    initStyleConfig.scale = 1;
    updateTreeData();
    drawMarker(d3.select(svgRef.current));
    handleLayout();
    updateRender();
    // handleZoom();
  }, [planSource]);

  const { cteAreas, links, mainWithCteLink, rects } = renderData;

   const [targetRef, isView] = useInView();
  useEffect(() => {
    if (!isView) {
      handleZoom();
    }
  }, [isView]);

  return (
    <div style={{ background: "#fff", height: "100%" }}>
      <svg
        xmlns="http://www.w3.org/2000/svg"
        xlinkHref="'http://www.w3.org/1999/xlink"
        width="100%"
        height="100%"
        ref={svgRef}
      >
        <g className="zoomG" ref={targetRef} transform={transform}>
          <g className="gCte">{mainWithCteLink}</g>
          <g className="cteAreas">{cteAreas}</g>
          <g className="gPath">{links}</g>
          <g className="gRect">{rects}</g>
        </g>
      </svg>
    </div>
  );
}
